/*
Copyright 2007-2009 Selenium committers

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */

package org.openqa.selenium.firefox;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.io.CharStreams;
import com.google.common.io.Closeables;
import com.google.common.io.LineReader;

import org.json.JSONException;
import org.json.JSONObject;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.remote.JsonException;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


class Preferences {

  /**
   * The maximum amount of time scripts should be permitted to run. The user may increase this
   * timeout, but may not set it below the default value.
   */
  private static final String MAX_SCRIPT_RUN_TIME_KEY = "dom.max_script_run_time";
  private static final int DEFAULT_MAX_SCRIPT_RUN_TIME = 30;
  
  /**
   * This pattern is used to parse preferences in user.js. It is intended to match all preference
   * lines in the format generated by Firefox; it won't necessarily match all possible lines that
   * Firefox will parse.
   * 
   * e.g. if you have a line with extra spaces after the end-of-line semicolon, this pattern will
   * not match that line because Firefox never generates lines like that.
   */
  private static final Pattern PREFERENCE_PATTERN =
      Pattern.compile("user_pref\\(\"([^\"]+)\", (\"?.+?\"?)\\);");

  private Map<String, Object> immutablePrefs = Maps.newHashMap();
  private Map<String, Object> allPrefs = Maps.newHashMap();

  public Preferences(Reader defaults) {
    readDefaultPreferences(defaults);
  }

  public Preferences(Reader defaults, File userPrefs) {
    readDefaultPreferences(defaults);
    FileReader reader = null;
    try {
      reader = new FileReader(userPrefs);
      readPreferences(reader);
    } catch (IOException e) {
      throw new WebDriverException(e);
    } finally {
      Closeables.closeQuietly(reader);
    }
  }

  public Preferences(Reader defaults, Reader reader) {
    readDefaultPreferences(defaults);
    try {
      readPreferences(reader);
    } catch (IOException e) {
      throw new WebDriverException(e);
    } finally {
      Closeables.closeQuietly(reader);
    }
  }

  private void readDefaultPreferences(Reader defaultsReader) {
    try {
      String rawJson = CharStreams.toString(defaultsReader);
      JSONObject jsonPrefs = new JSONObject(rawJson);

      JSONObject frozen = jsonPrefs.getJSONObject("frozen");
      Iterator keys = frozen.keys();
      while (keys.hasNext()) {
        String key = (String) keys.next();
        Object value = frozen.get(key);
        setPreference(key, value);
        immutablePrefs.put(key, value);
      }

      JSONObject mutable = jsonPrefs.getJSONObject("mutable");
      keys = mutable.keys();
      while (keys.hasNext()) {
        String key = (String) keys.next();
        Object value = mutable.get(key);
        setPreference(key, value);
      }
    } catch (JSONException e) {
      throw new JsonException(e);
    } catch (IOException e) {
      throw new WebDriverException(e);
    }
  }

  private void setPreference(String key, Object value) {
    if (value instanceof String) {
      setPreference(key, (String) value);
    } else if (value instanceof Boolean) {
      setPreference(key, ((Boolean) value).booleanValue());
    } else {
      setPreference(key, ((Number) value).intValue());
    }
  }

  private void readPreferences(Reader reader) throws IOException {
    LineReader allLines = new LineReader(reader);
    String line = allLines.readLine();
    while (line != null) {
      Matcher matcher = PREFERENCE_PATTERN.matcher(line);
      if (matcher.matches()) {
        allPrefs.put(matcher.group(1), preferenceAsValue(matcher.group(2)));
      }
      line = allLines.readLine();
    }
  }

  public void setPreference(String key, String value) {
    checkPreference(key, value);
    if (isStringified(value)) {
      throw new IllegalArgumentException(
          String.format("Preference values must be plain strings: %s: %s",
              key, value));
    }
    allPrefs.put(key, value);
  }

  public void setPreference(String key, boolean value) {
    checkPreference(key, value);
    allPrefs.put(key, value);
  }

  public void setPreference(String key, int value) {
    checkPreference(key, value);
    allPrefs.put(key, value);
  }

  public void addTo(Preferences prefs) {
    // TODO(simon): Stop being lazy
    prefs.allPrefs.putAll(allPrefs);
  }

  public void addTo(FirefoxProfile profile) {
    profile.getAdditionalPreferences().allPrefs.putAll(allPrefs);
  }

  public void writeTo(Writer writer) throws IOException {
    for (Map.Entry<String, Object> pref : allPrefs.entrySet()) {
      writer.append("user_pref(\"").append(pref.getKey()).append("\", ");
      writer.append(valueAsPreference(pref.getValue()).replaceAll("\\\\", "\\\\\\\\"));
      writer.append(");\n");
    }
  }

  private String valueAsPreference(Object value) {
    if (value instanceof String) {
      return "\"" + value + "\"";
    }

    return String.valueOf(value);
  }

  private Object preferenceAsValue(String toConvert) {
    if (toConvert.startsWith("\"") && toConvert.endsWith("\"")) {
      return toConvert.substring(1, toConvert.length() - 1).replaceAll("\\\\\\\\", "\\\\");
    }

    if ("false".equals(toConvert) || "true".equals(toConvert)) {
      return Boolean.parseBoolean(toConvert);
    }

    try {
      return Integer.parseInt(toConvert);
    } catch (NumberFormatException e) {
      throw new WebDriverException(e);
    }
  }

  @VisibleForTesting
  protected Object getPreference(String key) {
    return allPrefs.get(key);
  }

  private boolean isStringified(String value) {
    // Assume we a string is stringified (i.e. wrapped in " ") when
    // the first character == " and the last character == "
    return value.startsWith("\"") && value.endsWith("\"");
  }

  public void putAll(Map<String, Object> frozenPreferences) {
    allPrefs.putAll(frozenPreferences);
  }

  private void checkPreference(String key, Object value) {
    checkNotNull(value);
    checkArgument(!immutablePrefs.containsKey(key) ||
                  (immutablePrefs.containsKey(key) && value.equals(immutablePrefs.get(key))),
                  "Preference %s may not be overridden: frozen value=%s, requested value=%s",
                  key, immutablePrefs.get(key), value);
    if (MAX_SCRIPT_RUN_TIME_KEY.equals(key)) {
      int n;
      if (value instanceof String) {
        n = Integer.parseInt((String) value);
      } else if (value instanceof Integer) {
        n = (Integer) value;
      } else {
        throw new IllegalArgumentException(String.format(
            "%s value must be a number: %s", MAX_SCRIPT_RUN_TIME_KEY, value.getClass().getName()));
      }
      checkArgument(n == 0 || n >= DEFAULT_MAX_SCRIPT_RUN_TIME,
                    "%s must be == 0 || >= %s",
                    MAX_SCRIPT_RUN_TIME_KEY,
                    DEFAULT_MAX_SCRIPT_RUN_TIME);
    }
  }

}
